/* © 2007 Adobe Systems Incorporated. All rights reserved. */
package com.shubhjai.musiclib.data
{
	import com.shubhjai.musiclib.commands.SQLCommand;
	import com.shubhjai.musiclib.data.parsers.IDataParser;
	import com.shubhjai.musiclib.events.DataSynchronizationEvent;
	import com.shubhjai.musiclib.managers.ConfigManager;
	import com.shubhjai.musiclib.model.Record;
	
	import deng.fzip.FZip;
	import deng.fzip.FZipErrorEvent;
	import deng.fzip.FZipFile;
	
	import flash.data.SQLResult;
	import flash.errors.SQLError;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SQLEvent;
	import flash.net.Responder;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.utils.getTimer;
	import flash.utils.getDefinitionByName;
	import flash.filesystem.File;
	import flash.events.HTTPStatusEvent;
	import flash.events.SQLErrorEvent;

	/** 
	 * An event indicating that synchronization has started. 
	 *  @eventType com.shubhjai.musiclib.events.DataSynchronizationEvent.SYNC_START
	 */
	[Event(name="syncStart", type="com.shubhjai.musiclib.events.DataSynchronizationEvent")]
	
	/** 
	 * An event indicating that synchronization has completed. 
	 * @eventType com.shubhjai.musiclib.events.DataSynchronizationEvent.SYNC_COMPLETE
	 */
	[Event(name="syncComplete", type="com.shubhjai.musiclib.events.DataSynchronizationEvent")]
	

	/**
	 * A command to synchronize the database to an external data source. This class is designed to
	 * work with a parameterizable parser class for different data sources (e.g. XML vs. CSV), and 
	 * also will conditionally unzip a file if a zip file is provided.
	 */ 
	public class SynchronizeDatabaseCommand extends SQLCommand
	{
		private var employees : Array;
		private var configManager : ConfigManager;
		private var libraryDataURL : String;
		private var syncTimeout : uint;
		private var forceSync : Boolean;
		private var insertResponder : Responder;
		
		/**
		 * Constructor
		 * @param conn The database upon which we execute the synchronization
		 * @param forceSync True if we should force synchronization regardless of the time comparison.
		 */ 
		public function SynchronizeDatabaseCommand( forceSync:Boolean = false ) 
		{
			this.forceSync = forceSync;
			configManager = ConfigManager.getInstance();
			libraryDataURL = configManager.getProperty( "libraryDataURL" );
			syncTimeout = configManager.getNumericProperty( "syncTimeoutHourInterval" ) * 60 * 60 * 1000;
			if ( isNaN( syncTimeout ) )
			{
				syncTimeout = 24 * 60 * 60 * 1000;
			}
		}
		
		/**
		 * Execute the SQL.
		 */ 
		override protected function executeSQL() : void
		{
			if ( forceSync )
			{
				refreshData();
			}
			else
			{
				stmt.text = "SELECT lastUpdated from updatelog";
				stmt.execute( -1, new Responder( onCheckLastRead, onSQLError ) );
			}
		}
		
		private function onCheckLastRead( res:SQLResult ) : void
		{
			logDebug("onCheckLastRead()");
			
			// TODO: Make a better date check. Right now we just check that there are no records.
			var lastRead : Number = res.data[0]["lastUpdated"];
			
			if ( lastRead > 0 )
			{
				if ( lastRead + syncTimeout < new Date().getTime() )
				{
					refreshData();
				}
				else
				{
					notifyComplete();
				}
			}
			else
			{
				refreshData();
			}

		}
			
		
		/**
		 * Refresh teh data source.
		 */ 
		private function refreshData() : void
		{
			logDebug("refreshData(): " + libraryDataURL);
			 
			if ( libraryDataURL == null )
			{
				notifyError( "Error loading data. No 'libraryDataURL' config property defined." );
				return;
			}
			
			if ( libraryDataURL.indexOf(".zip") > 0 )
			{
				
				
				// BUG: For some reason the HTTP status from the FZip utility does not get called correctly if the ZIP
				// is not available (offline use). Therefore, a parse error gets generated instead. We case for that already,
				// so just use it.
				var zip : FZip = new FZip();
				zip.addEventListener( Event.COMPLETE, onZipLoadComplete );
				zip.addEventListener( IOErrorEvent.IO_ERROR, onLoadError );
				//zip.addEventListener( HTTPStatusEvent.HTTP_STATUS, onZipLoadStatus );
				zip.addEventListener( FZipErrorEvent.PARSE_ERROR, onZipParseError );
				zip.load( new URLRequest( libraryDataURL ) );
			} 
			else
			{	
				var ldr : URLLoader = new URLLoader( new URLRequest( libraryDataURL ) );
				ldr.addEventListener( Event.COMPLETE, onLoadComplete );
				ldr.addEventListener( IOErrorEvent.IO_ERROR, onLoadError );	
			}
			
		}
		
		private function onZipLoadComplete( evt:Event ) : void
		{
			logDebug("onZipLoadComplete()");
			var zip : FZip = evt.target as FZip;
			var zipFile : FZipFile = zip.getFileAt( 0 );
			
			var data : Object = zipFile.content;
			
			var start : Number = getTimer();
			processData( data );
	
			removeZipListeners( zip );

		
			logDebug("Zip Parse time: " + ( getTimer() - start ) );
		}
		
		/*private function onZipLoadStatus( evt:HTTPStatusEvent ) : void
		{
			logDebug("onZipLoadStatus: " + evt.status);
			var zip : FZip = evt.target as FZip;
			removeZipListeners( zip );
		}*/
		
		private function onZipParseError( evt:FZipErrorEvent ) : void
		{
			logDebug("onZipParseError()");
			var zip : FZip = evt.target as FZip;
			removeZipListeners( zip );
			notifyError( evt.text );
		}
		
		
		private function removeZipListeners( zip:FZip ) : void
		{
			zip.removeEventListener( Event.COMPLETE, onZipLoadComplete );
			zip.removeEventListener( IOErrorEvent.IO_ERROR, onLoadError );
			//zip.removeEventListener( HTTPStatusEvent.HTTP_STATUS, onZipLoadstatus );
			zip.removeEventListener( FZipErrorEvent.PARSE_ERROR, onZipParseError );
			zip.close();	
		
		}
		
		private function onLoadComplete( evt:Event ) : void
		{
			logDebug("onLoadComplete()");
			
			var ldr : URLLoader = evt.target as URLLoader;
			ldr.removeEventListener( Event.COMPLETE, onLoadComplete );
			ldr.removeEventListener( IOErrorEvent.IO_ERROR, onLoadError );
			ldr.close();
			
			var data : Object = ldr.data;
			
			
			var start : Number = getTimer();
			processData( data );
			
			ldr.data = null;
			data = null;
			ldr = null;
			logDebug("Done parsing data. Time: " + ( getTimer() - start ) );
		}
		
		private function processData( data:Object ) : void
		{
		
			// We got our data. w00t! Inform others that we are started syncing.
			dispatchEvent( new DataSynchronizationEvent( DataSynchronizationEvent.SYNC_START ) );
				
			var recordParserClassName : String = configManager.getProperty( "recordParserClass" );
			if ( recordParserClassName == null )
			{
				notifyError( "Error parsing data. No 'recordParserClass' config property defined." );
			}
			else
			{
			  	try {
					var parser : IDataParser = new ( getDefinitionByName( recordParserClassName ) ) as IDataParser;
			  	}
			  	catch ( e:Error ) 
			  	{
			  		notifyError( "Error instantiating parser - " + recordParserClassName );
			  		return;
			  	}
			  		
			  	try {
			  		employees = parser.parse( data );
			  		parser = null;
			  	}
			  	catch ( e:Error )
			  	{
			  		notifyError( "Error parsing employee data - " + e.message );
			  		throw (e);  // for debugging
			  		return;
			  	}
			  	
			  	logDebug("beginning transaction.");
			  	conn.addEventListener( SQLEvent.BEGIN, onBegin );
			  	conn.begin(); //SQLTransactionLockType.IMMEDIATE );
			}
		}
			
		private function onBegin( evt:SQLEvent ) : void
		{
			// first, delete all the existing data.
			conn.removeEventListener( SQLEvent.BEGIN, onBegin );
			stmt.text = "delete from employee";
			stmt.execute( -1, new Responder( loadData, onSQLError ) );
		}
		
		
		
		private function loadData( res:SQLResult ) : void
		{
			 // This has city, state, postalcode, countrycode
				stmt.text = "INSERT INTO employee ('id', 'firstName', 'lastName', 'displayName', 'title', 'department', 'managerId', 'email', " + 
					"'phone', 'phoneExtension', 'cellPhone', 'deskLocation', 'location', 'city', 'state', 'postalCode', 'countryCode') " + 
					" VALUES (:id, :firstName, :lastName, :displayName, :title, :department, :managerId, :email," +
					" :phone, :phoneExtension, :cellPhone, :deskLocation, :location, :city, :state, :postalCode, :countryCode);";

			insertResponder = new Responder( onInsertSuccess, onSQLError );
			insertNextRecord();
		}

		/**
		 * Callback when we successfully insert all records. 
		 */ 
		private function onInsertSuccess( res:SQLResult ) : void
		{
			//logDebug("onInsertSuccess: " + dataArr.length);
			if ( employees.length == 0 )
			{
				stmt.clearParameters();
			
				// NOTE: Update the appVersion here so that only on a succesful sync do we consider ourselves updated. 
				var time : String = String( new Date().getTime() );
				var appVersion : String = configManager.getProperty( "appVersion" );
				
			
				stmt.text = "UPDATE updatelog set lastUpdated = '" + time + "', appVersion = '" + appVersion + "';" ;
				stmt.execute( -1, new Responder( updateComplete, onSQLError ) );
			}
			else
			{
				insertNextRecord();
			}
		}
		
		
		private function updateComplete( evt:SQLResult ) : void
		{	
			logDebug("updateComplete(): " + stmt.executing);
			doCommit();
		}
		
		
		private function doCommit() : void
		{
			logDebug( "doCommit()" );
			
			conn.addEventListener( SQLEvent.COMMIT, onCommit );
			conn.addEventListener( SQLErrorEvent.ERROR, onCommitError );
			conn.commit();
		}
		
		private function onCommit( evt:SQLEvent ) : void
		{
			conn.removeEventListener( SQLEvent.COMMIT, onCommit );
			conn.removeEventListener( SQLErrorEvent.ERROR, onCommitError );
			
			// delete the local cache of images to conform to company policy
			var imageDir : File = File.applicationStorageDirectory.resolve( "images/" );
			if ( imageDir.exists )
				imageDir.deleteDirectory( true );
			

			dispatchEvent( new DataSynchronizationEvent( DataSynchronizationEvent.SYNC_COMPLETE ) );
			notifyComplete();
		}
		
		private function onCommitError( evt:SQLErrorEvent ) : void
		{
			logError( "onCommitError: " + evt );
			conn.removeEventListener( SQLEvent.COMMIT, onCommit );
			conn.removeEventListener( SQLErrorEvent.ERROR, onCommitError );
			
			conn.rollback();
			notifyError( "Error committing transaction: " + evt );
			//doCommit();
		}
		
		
		override protected function onSQLError( err:SQLError ) : void
		{
			var msg : String = "An error occurred executing the SQL : " + err.errorID + " : " + err.message + " - " + stmt.text;
			logDebug( msg );
			
			conn.rollback();
	
			//dispatchEvent( new DataSynchronizationEvent( DataSynchronizationEvent.SYNC_COMPLETE ) );
			notifyError( msg );
		}

		private function insertNextRecord() : void
		{
			var employee : Record = employees.shift() as Record;
				
			stmt.parameters[":id"] = employee.id;
			stmt.parameters[":firstName"] =  employee.firstName;
			stmt.parameters[":lastName"] = employee.lastName;
			stmt.parameters[":displayName"] = employee.displayName;
			stmt.parameters[":title"] =  employee.title;
			stmt.parameters[":department"] =  employee.department;
			stmt.parameters[":email"] =  employee.email;
			stmt.parameters[":phone"] =  employee.phone;
			stmt.parameters[":managerId"] = employee.managerId;
			stmt.parameters[":phoneExtension"] =  employee.phoneExtension;
			stmt.parameters[":cellPhone"] = employee.cellPhone;
			stmt.parameters[":deskLocation"] = employee.deskLocation;
			stmt.parameters[":city"] =  employee.city;
			stmt.parameters[":location"] =  employee.location;
			stmt.parameters[":state"] =  employee.state;
			stmt.parameters[":countryCode"] =  employee.countryCode;
			stmt.parameters[":postalCode"] =  employee.postalCode;
			stmt.execute( -1, insertResponder );
		}
		
		private function onLoadError( evt:IOErrorEvent ) : void
		{
			logDebug("onLoadError()");
			// this is an expected error in offline mode.
			notifyError( "Error loading data source - " + libraryDataURL );
		}
		
	
	}	
}